<?php
require_once 'i_string.php';

/** Represent a menu item which may have child menu items. */
class MenuItem {
	/** 
	 * Optional ID of the menu item.
	 * @var string
	 */
	public $id;
	/**
	 * Relative path to the URL root (may have GET params).
	 * e.g., 'home/index.php', 'product/show.php?id=1&print=false' (GET params must be in alphabeta order)
	 * @var String */
	private $path;
	
	/**
	 * The applicable language or empty if available for all languages - control the visibility of this menu item.
	 * @var string */
	private $language;
	
	/**
	 * The applicable country or empty if available for all countries - control the visibility of this menu item.
	 * @var string */
	private $country;
	
	/** The applicable roles or empty if available for all roles  - control the visibility of this menu item.
	 * The user should have at least one role matched in this array.
	 * @var array */
	private $roles;
	
	/** 
	 * Label text in English
	 * @var String */
	private $label;
	/** 
	 * Label text in other languages, with language code as keys and text as values.
	 * @var array */
	private $label_i18ns;
	/** 
	 * Tooltip to be displayed in Chinese if any 
	 * @var String */
	private $tooltip_zh;
	/** 
	 * Tooltip to be displayed in English if any
	 * @var String */
	private $tooltip_en;
	/** Child menu items - type: array (hashmap of item path to item object) 
	 * @var array */
	private $subitems;
	
	/**
	 * The parent menu item or NULL for root
	 * @var MenuItem */
	private $parent;
	
	/**
	 * Constructor.
	 * @param $id the optional id of the menu item.
	 * @param $path url relative to root, e.g., 'home/index.php', 'product/show.php?id=1&print=false' (GET params must be in alphabeta order)
	 * @param $language The applicable language or empty if available for all languages - control the visibility of this menu item.
	 * @param $country The applicable country or empty if available for all countries - control the visibility of this menu item.
	 * @param $roles The applicable roles separated in commar or empty if available for all roles  - control the visibility of this menu item.
	 * @param $label Label in English
	 * @param $label_i18n Label text in other languages, must be in this format {fr=Text in french}{zh-tw=text in zh-tw}
	 * @param $subitems array of initial sub items or NULL.
	 */
	public function __construct($id, $path, $language, $country, $roles, $label, $label_i18n, array $subitems = NULL) {
		$this->id = $id;
		$this->path = $path;
		$this->language = $language;
		$this->country = $country;
		$this->roles = strlen($roles) > 0 ? explode(",", $roles) : null;
		$this->label = $label;
		$this->label_i18ns = strlen($label_i18n) > 0 ? StringUtil::parseI18n($label_i18n) : null;
		if(is_array($subitems)) {
			for($i = 0; $i < count($subitems); $i++) {
				$this->addSubItem($subitems[$i]);
			}
		}
	}

	/** 
	 * Whether this item is root, i.e., no parent. 
	 * @return boolean
	 */
	public function isRoot() {
		return $this->parent == NULL;
	}
	
	public function getPath() {
		return $this->path;
	}
	
	/** 
	 * Returns the depth level of the menu item, 0 for root 
	 * @return int
	 */
	public function getLevel() {
		$p = $this;
		$count = 0;
		while(!$p->isRoot()) {
			$count ++;
			$p = $p->parent;
		}
		return $count;
	}
	
	// caches last computed label. 
	private $lastLabelLang;
	private $lastLabelText;
	/**
	 * Returns the proper label according to the requested language.
	 * Current return label_zh for all langs starting with 'zh' and label_en for all other langs.
	 * @param $lang ISO639-1 lang code, e.g., 'en-us', 'zh', 'zh-tw'
	 * @return string
	 */
	public function getLabel($lang) {
		if(strlen($lang) == 0 || substr($lang, 0, 2) == "en") {
			return $this->label;
		} else {
			if($lang == $this->lastLabelLang && $this->lastLabelText) { // use cache.
				return $this->lastLabelText;
			}
			if(is_array($this->label_i18ns)) {
				foreach($this->label_i18ns as $i18nlang => $i18ntext) {
					if($i18nlang == $lang) {
						$matchExact = $i18ntext;
					}else if(substr($i18nlang, 0, 2) == substr($lang, 0, 2)) { // belong to same lang category
						$matchCategory = $i18ntext;
					}
				}
				$this->lastLabelLang = $lang;
				$this->lastLabelText = StringUtil::firstNonEmptyString($matchExact, $matchCategory, $this->label);
				return $this->lastLabelText;
			}else{
				return $this->label; // no i18n defined, return English.
			}
		}
	}

	
	/** Adds the given item as a sub item */
	public function addSubItem(MenuItem $item) {
		// print_r($item);
		if($item == NULL) {
			return;
		}
		if(! ($item instanceof MenuItem)) {
			throw new Exception("Expected: MenuItem; actual: " + get_class($item));
		}
		if(! is_array($this->subitems)) {
			$this->subitems = array();
		}
		$this->subitems[$item->path] = $item;
		$item->parent = $this; // set parent.
		//array_push($this->subitems, $item);
	}
	
	/**
	 * Returns the sub item with the given ID.
	 * @param $id id for search
	 * @param $recursive false to search first level only (children); otherwise all depth levels (decendants).
	 * @return MenuItem
	 */
	public function getSubItemById($id, $recursive) {
		// echo "Searching $this->id<br>";
		if($this->subitems == NULL) {
			return NULL;
		}
		if($this->subitems != NULL) {
			reset($this->subitems);
			while($key = key($this->subitems)) {
				$item = $this->subitems[$key];
				if($item->id == $id) {
					return $item;
				} else {
					$found = $item->getSubItemById($id, $recursive);
					if($found != NULL) {
						return $found;
					}
				}
				next($this->subitems);
			}
			return NULL;
		}
	}
	
	/**
	 * Do a recursive lookup for the given path and returns the menu item chain as an array
	 * @param $path the path to be searched
	 * @return array of MenuItem in root to leaf order or NULL if not found.
	 */
	public function searchPath( $path) {
		if($this->path == $path) { // this is the leaf.
			// Check one level down to see whether there is any 'index' page (i.e., page with the same as the parent).
			$childWithSamePath = is_array($this->subitems) ? $this->subitems[$path] : null;
			return $childWithSamePath == null ? array($this) : array($this, $childWithSamePath);
		}elseif ($this->subitems != NULL) { // search child.
			reset($this->subitems);
			while($key = key($this->subitems)) {
				$subitem = $this->subitems[$key];
				$found = $subitem->searchPath($path);
				if(is_array($found)) { // found
					array_unshift($found, $this);
					return $found; // return here.	
				}else{
					next($this->subitems);
				}
			}
			// not in this branch.
			return NULL;
		}else {
			return NULL;
		}
	}
	
	/**
	 * Return true if this menu item should be visible in conditions specified by the arguments.
	 * @param $language ISO639-1 lang code, e.g., 'en-us', 'zh', 'zh-tw' - used to filter menu itmes.
	 * @param $country the 2-letter lowercased country code as defined in ISO639-1 - used to filter menu itmes.
	 * @param $roles the array of roles held by the current user - used to filter menu itmes.
	 * @return bool true if matched or false otherwise
	 */
	public function match($language, $country, $roles) {
		if(strlen($this->language) > 0 && $this->language != $language) {
			return false;
		}
		if(strlen($this->country) > 0 && $this->country != $country) {
			return false;
		}
		if((is_array($this->roles) && count($this->roles) > 0) && (!is_array($roles) || count(array_intersect($this->roles, $roles)) == 0)) {
			return false;
		}
		return true;
	}
	
	/**
	 * Returns the corresponding HTML code.
	 * @param $selectedItems array of selected item chain from root to leaf or NULL if no selection.
	 * @param $language ISO639-1 lang code, e.g., 'en-us', 'zh', 'zh-tw' - used to filter menu itmes.
	 * @param $country the 2-letter lowercased country code as defined in ISO639-1 - used to filter menu itmes.
	 * @param $roles the array of roles held by the current user or null - used to filter menu itmes.
	 * @param $printThis false indicating do not print this but print its child items.
	 * @param $printSubItemsLevel the levels of sub items to be printed - 0 for no, 1 for one level only.
	 * @param $printSubForSelectedOnly if true, only sub items in the selected path shall be included otherwise all.
	 * @param $renderFuncForListItemContent name of explicit function (params: MenuItem, selected, lang, country) for rendering list item content or null to render in default format (&lt;a /&gt;).
	 * @param $addtionalClass additional class name to be added to this.
	 * @return string
	 */
	public function toHtmlMenu($selectedItems, $language, $country, $roles, $printThis = true, 
	  $printSubItemsLevel = 100, $printSubForSelectedOnly = true, $renderFuncForListItemContent = NULL,
	  $addtionalClass = "") {
		global $URL_ROOT_WITH_CC_LG;
		// echo $renderFuncForListItemContent;
		$selectIndex = $selectedItems == NULL ? false : array_search($this, $selectedItems);
		if($printThis) {
			$s = "<li class=\"" . ($selectIndex === false ? "" : "selected") . " $addtionalClass\">";
			if($renderFuncForListItemContent == null) {
				$s .= "<a href=\"$URL_ROOT_WITH_CC_LG$this->path\">" . $this->getLabel($language) . "</a>";
			}else{
				$s .= call_user_func($renderFuncForListItemContent, $this, $selectIndex, $language, $country);
			}
		}else{
			$s = "";
		}
		if($printSubItemsLevel > 0 && is_array($this->subitems) && (!$printSubForSelectedOnly || $selectIndex !== false)) { // only selected
			if($selectIndex !== false) {
				$s .= "<ul class=\"$language selected\">\n";
			}else{
				$s .= "<ul class=\"$language\">\n";
			}
			
			/* @var $subitem MenuItem */
			$count = 0;
			foreach($this->subitems as $key => $subitem) { // print sub items.
				if($subitem->match($language, $country, $roles)) { // only if matched.
					$s .= $subitem->toHtmlMenu($selectedItems, $language, $country, $roles, true, 
						$printSubItemsLevel - 1, $printSubForSelectedOnly, $renderFuncForListItemContent,
						$count == 0 ? "first" : ($count == sizeof($this->subitems) - 1 ? "last" : ""));
				}else{
					// $s .= "<li>Not matched: " . $subitem;
				}
				$count ++;
			}
			$s .= '</ul>';
		}
		if($printThis) {
			$s .= "</li>\n";
		}
		return $s;
	}
	
	public function __toString() {
		$s = $this->path . " (" . $this->label_zh . "/" . $this->label_en . ")" . 
			(empty($this->subitems) ? '' : '[' . count($this->subitems) . ']');
			
		if(strlen($this->language) > 0) {
			$s .= " filter lang: " . $this->language;
		}
		if(strlen($this->country) > 0) {
			$s .= " filter country: " . $this->country;
		}
		if(is_array($this->roles) && count($this->roles) > 0) {
			$s .- " filter roles: " . $this->roles;
		}

		if(false && count($this->subitems) > 0) {
			$s .= "{";
			reset($this->subitems);
			while($key = key($this->subitems)) {
				$s .= $this->subitems[$key];
				$s .= ', ';
				next($this->subitems);
			}
			$s = substr($s, 0, strlen($s) - 2); // remove last ', '
			$s .= "}";
		}	
			
		return $s;
	}
}

/**
 * Returns the url path relative to URL root with get params (except PHPSESSIONID, cc and lg).
 * @return string relative url path to url root.
 */
function getURLForThisPage() {
	global $URL_ROOT;
	if(empty($URL_ROOT)) {
		throw new Exception('$URL_ROOT is not defined or the defining file is not included.');
	}
	$url = substr($_SERVER['PHP_SELF'], strlen($URL_ROOT));
	$url .= getOrderedGetParams($_GET); // i.e., $_GET	
	return $url;
}

/**
 * Reforms the specified URL's var part (only this query part) in order so that it can be used in menu locating. 
 * this function returns something like this: "?a=b&zoo=monkey%20a" in key sorted order.
 */
function getOrderedGetParams($vars) {
	ksort($vars);
	if (empty ($vars) || (!is_array($vars)))
		return '';
	$str = "?";
	while (list ($key, $value) = each($vars)) {
		if ($key == "PHPSESSID" || $key == "cc" /*Country::VAR_NAME*/ || $key == "lg" /*Language::VAR_NAME*/) { // escape: session, country, language
			continue;
		}

		if (strlen($str) > 1)
			$str .= "&";
		$str .= rawurlencode($key);
		$str .= "=";
		$str .= rawurlencode($value);
	}
	return strlen($str) > 1 ? $str : "";
}

?>